package com.alcatel_lucent.osp.service;

import com.alcatel_lucent.osp.action.UserContext;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
import net.sf.json.processors.PropertyNameProcessor;
import net.sf.json.util.PropertyFilter;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import static java.lang.Math.ceil;

@Service("jsonService")
@SuppressWarnings("unchecked")
public class JSONServiceImpl implements JSONService {

    private static Logger log = LoggerFactory.getLogger(JSONServiceImpl.class);
    private static final DateFormat dFmt = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss");

    @Autowired
    protected DaoService dao;

    public String toJSONString(Object entity, String propExp) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        return toJSON(entity, propExp).toString();
    }

    public JSON toJSON(Object entity, String propExp) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        if (entity instanceof Collection) {
            JSONArray jsonArray = new JSONArray();
            for (Object obj : (Collection<Object>) entity) {
                jsonArray.add(toJSON(obj, propExp));
            }
            return jsonArray;
        }
        JSONObject json = new JSONObject();
        String[] props = extractFirstLevelProperties(propExp);
        for (String prop : props) {
            try {
                int pos = prop.indexOf("{");
                if (pos == -1) {
                    json.put(prop, PropertyUtils.getProperty(entity, prop.trim()));
                } else {
                    String refProp = prop.substring(0, pos).trim();
                    Object refObject = PropertyUtils.getProperty(entity, refProp);
                    if (refObject == null) {
                        json.put(refProp, null);
                    } else {
                        json.put(refProp, toJSON(refObject, prop.substring(pos)));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                log.error(e.toString());
            }
        }
        return json;
    }


    private Collection retrieve(String hql, Map<?, ?> param, String countHql, Map<?, ?> countParam, Map<String, String> requestMap) {
        String rows = requestMap.get("rows");
        String page = requestMap.get("page");

        if (null == rows) {// not paged
            return dao.retrieve(hql, param);
        }
        //paged
        int first = (page == null ? 0 : (Integer.parseInt(page) - 1) * Integer.parseInt(rows));
        // count total records
        if (null != countHql) {
            Number records = (Number) dao.retrieveOne(countHql, countParam);
            requestMap.put("records", records == null ? "0" : records + "");
        }
        return dao.retrieve(hql, param, first, Integer.parseInt(rows));
    }

    public JSON getJSON(Map<String, String> requestMap, String hql, Map<?, ?> param, String countHql, Map<?, ?> countParam) throws JSONException {
        Object data = retrieve(hql, param, countHql, countParam, requestMap);
        if (data == null) {
            return new JSONObject();
        }

        String format = requestMap.get("format");
        String prop = requestMap.get("prop");
        String idprop = requestMap.get("idprop");

        try {
            if (format == null) {
                return toJSON(data, prop);
            } else if (format.trim().equals("grid")) {
                Integer rows = requestMap.get("rows") == null ? null : Integer.valueOf(requestMap.get("rows"));
                Integer page = requestMap.get("page") == null ? null : Integer.valueOf(requestMap.get("page"));
                Integer records = requestMap.get("records") == null ? null : Integer.valueOf(requestMap.get("records"));
                JSONObject json = toGridJSON((Collection<?>) data, rows, page, records, idprop == null ? "id" : idprop, prop);
                return json;
            } else if (format.trim().equals("tree")) {
                String idProps = requestMap.get("idProps");
                String types = requestMap.get("types");
                String dataProps = requestMap.get("dataProps");
                String childrenProps = requestMap.get("childrenProps");
                JSONObject json = toTreeJSON(data, idProps.split(","), types.split(","), dataProps.split(","), childrenProps.split(","));
                return json;
            } else {
                throw new JSONException("Unknown format '" + format + "'");
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.toString());
            throw new JSONException(e);
        }
    }

    private String[] extractFirstLevelProperties(String propExp) {
        propExp = propExp.trim();
        if (propExp.startsWith("{") && propExp.endsWith("}")) {
            propExp = propExp.substring(1, propExp.length() - 1).trim();
        }
        ArrayList<String> result = new ArrayList<String>();
        StringBuffer item = new StringBuffer();
        int deep = 0;
        for (int i = 0; i < propExp.length(); i++) {
            if (propExp.charAt(i) == '{') {
                deep++;
                item.append(propExp.charAt(i));
            } else if (propExp.charAt(i) == '}') {
                deep--;
                item.append(propExp.charAt(i));
            } else if (propExp.charAt(i) == ',' && deep == 0) {
                result.add(item.toString().trim());
                item = new StringBuffer();
            } else {
                item.append(propExp.charAt(i));
            }
        }
        if (!item.toString().isEmpty()) {
            result.add(item.toString().trim());
        }
        return result.toArray(new String[0]);
    }


    @Override
    public JSONObject toGridJSON(Collection<?> entities, Integer rows, Integer page, Integer records, String idProp, String cellProps) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        JSONArray jsonArrayGrid = new JSONArray();
        JSONObject jsonGrid = new JSONObject();

        for (Object entity : entities) {
            JSONObject jsonRow = new JSONObject();
            if (idProp != null) {
                jsonRow.put("id", PropertyUtils.getProperty(entity, idProp.trim()));
            }
            JSONArray jsonCell = new JSONArray();
            String[] propArray = cellProps.split(",");
            for (String prop : propArray) {
                Object value = null;
                try {
                    value = PropertyUtils.getProperty(entity, prop.trim());
                } catch (Exception e) {
                    log.error(e.toString());
                }

                if (value != null) {    // escape html tags in grid result
                    if (value instanceof Date) {
                        UserContext uc = UserContext.getInstance();
                        TimeZone timeZone = null == uc ? TimeZone.getDefault() : uc.getTimeZone();
                        String strTmp = null == uc ? "server" : "client";
                        dFmt.setTimeZone(timeZone);
                        log.debug("format Date with {} time zone {}", strTmp, timeZone);
                        value = dFmt.format((Date) value);
                    } else {
                        value = StringEscapeUtils.escapeHtml(value.toString());
                    }
                } else {
                    value = StringUtils.EMPTY;
                }
                jsonCell.add(value);
            }
            jsonRow.put("cell", jsonCell);
            jsonArrayGrid.add(jsonRow);
        }

        if (rows != null && page != null) {
            int totalPages = (records != null && records > 0) ? (int) ceil(records / (float) rows) : 0;
            if (page > totalPages) page = totalPages;

            jsonGrid.put("page", page);
            jsonGrid.put("total", totalPages);
            jsonGrid.put("records", records);

        } else {
            jsonGrid.put("records", entities.size());
        }
        jsonGrid.put("rows", jsonArrayGrid);

//        Map<String, Object> userData=new HashMap<String, Object>();
//        jsonGrid.put("userData",userData);

        return jsonGrid;
    }

    private static String REMOTE_HOST = "http://us70uuapp029.zam.alcatel-lucent.com";
    private static String REMOTE_PATH = "/project/api/";
    private static Map<String, URL> REMOTE_URLS = new HashMap<String, URL>();

    public static final String COMPONENT_AND_RELEASE = "componentAndRelease";
    public static final String PWD_TO_LOCAL = "changeEPMBasedPWSToLocal";
    public static final String PROJECT_TO_DISABLE = "projectToDisable";
    public static final String PROJECT_MANAGER = "projectManager";
    public static final String POST_JIRA = "createJiraEntry";

    public static final String RESOURCE_NAME_KEY = "resources";


    static {
        String remoteURL = REMOTE_HOST + REMOTE_PATH;
        try {
            REMOTE_URLS.put(COMPONENT_AND_RELEASE, new URL(remoteURL + "epg_comp_and_release.php"));
            REMOTE_URLS.put(PWD_TO_LOCAL, new URL(remoteURL + "epg_pws_for_chg_local.php"));
            REMOTE_URLS.put(PROJECT_TO_DISABLE, new URL(remoteURL + "epg_pws_for_disable.php"));
            REMOTE_URLS.put(PROJECT_MANAGER, new URL(remoteURL + "epg_project_manager.php"));
            REMOTE_URLS.put(POST_JIRA, new URL(remoteURL + "create_jira_issue.php"));
//            REMOTE_URLS.put(POST_JIRA, new URL("http://locationinmotion.sinaapp.com/test/DaoServiceTest.php"));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }


    @Override
    public JSON getRemoteJSON(Map<String, String> postData) throws IOException {

        String resources = postData.get(RESOURCE_NAME_KEY);
        if (null == resources) resources = COMPONENT_AND_RELEASE;
        URL url = REMOTE_URLS.get(resources);
        log.info("url={}, postData={}", url, postData);
        postData.remove(RESOURCE_NAME_KEY);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        connection.setRequestMethod("POST");
        connection.setDoOutput(true);

        StringBuilder postDataStr = new StringBuilder();
        for (Map.Entry<String, String> param : postData.entrySet()) {
            if (postDataStr.length() != 0) postDataStr.append('&');
            postDataStr.append(String.format("%s=%s",
                    URLEncoder.encode(param.getKey(), "UTF-8"),
                    URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8")));
        }
        byte[] postDataBytes = postDataStr.toString().getBytes("UTF-8");

        connection.getOutputStream().write(postDataBytes);
        connection.connect();

        String resultString = IOUtils.toString(connection.getInputStream());

        JSON json = null;
        try {
            json = JSONArray.fromObject(resultString);
        } catch (net.sf.json.JSONException e) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", -1);
            map.put("message", "got result: " + resultString);
            json = JSONObject.fromObject(map);
        }
        connection.disconnect();
        return json;
    }

    @Override
    public Object getJSONResource(Map<String, String> postData) throws IOException {
        JSON json = this.getRemoteJSON(postData);
        if (!json.isArray() || json.size() == 0) return null;
        JSONArray jsonArray = (JSONArray) json;
        if (!(jsonArray.get(0) instanceof Map)) return Arrays.asList(jsonArray.toArray());

        Map<String, String> result = new LinkedHashMap<String, String>();
        for (Object object : jsonArray) {
            Map<String, String> objectMap = null;
            objectMap = (Map<String, String>) object;
            result.put(objectMap.get("id"), objectMap.get("name"));
        }
        return result;
    }

    public JSONArray toTreeJSON2(Object entity, Map<String, Collection<String>> propFilter, Map<Class, Map<String, String>>... vpropRename) {

        JsonConfig config = getJsonConfig(propFilter, vpropRename);
        JSONArray jsonTree = JSONArray.fromObject(entity, config);
        Collection<Map.Entry> entries = findEntry(jsonTree, "attr");
        for (Map.Entry entry : entries) {
            entry.setValue(JSONObject.fromObject(String.format("{'id':%d}", entry.getValue())));
        }
        return jsonTree;
    }


    public JSONObject toTreeJSON(Object root, String[] idProp, String[] types, String[] dataProp, String[] childrenProp) {
        JSONObject result = new JSONObject();
        try {
            Object id = PropertyUtils.getProperty(root, idProp[0]);
            Object data = PropertyUtils.getProperty(root, dataProp[0]);
            JSONObject jsonAttr = new JSONObject();
            jsonAttr.put("id", id);
            jsonAttr.put("type", types[0]);
            result.put("attr", jsonAttr);
            result.put("data", data);
            if (idProp.length > 1 && types.length > 1 && dataProp.length > 1 && childrenProp.length > 0) {
                Object children = PropertyUtils.getProperty(root, childrenProp[0]);
                if (children != null && children instanceof Collection) {
                    JSONArray jsonChildren = new JSONArray();
                    for (Object obj : (Collection<?>) children) {
                        jsonChildren.add(toTreeJSON(obj, next(idProp), next(types), next(dataProp), next(childrenProp)));
                    }
                    result.put("children", jsonChildren);
                }
            }
            return result;
        } catch (Exception e) {
            log.error("Failed to populate tree json of object " + root, e);
            return result;
        }
    }

    private String[] next(String[] arr) {
        String[] result = new String[arr.length - 1];
        for (int i = 0; i < result.length; i++) {
            result[i] = arr[i + 1];
        }
        return result;
    }


    public JSONArray toSelectJSON(Object entity, Map<String, Collection<String>> propFilter, Map<Class, Map<String, String>>... vpropRename) {
        JsonConfig config = getJsonConfig(propFilter, vpropRename);

        JSONArray jsonTree = JSONArray.fromObject(entity, config);
        return jsonTree;
    }

    /**
     * Construct JsonConfig according to the given parameter
     */
    private JsonConfig getJsonConfig(final Map<String, Collection<String>> propFilter, Map<Class, Map<String, String>>... vpropRename) {
        JsonConfig config = new JsonConfig();
        final Map<Class, Map<String, String>> propRename = vpropRename.length > 0 ? vpropRename[0] : new HashMap<Class, Map<String, String>>();

        config.setJsonPropertyFilter(new PropertyFilter() {
            @Override
            public boolean apply(Object source, String name, Object value) {
                String className = source.getClass().getName();
                int pos = className.lastIndexOf(".");
                if (pos != -1) {
                    className = className.substring(pos + 1);
                }
                Collection<String> props = propFilter.get(className);
                if (props != null) {
                    return !props.contains(name);
                }
                return true;
            }
        });

        //register rename property
        for (final Map.Entry<Class, Map<String, String>> entry : propRename.entrySet()) {
            config.registerJsonPropertyNameProcessor(entry.getKey(), new PropertyNameProcessor() {
                @Override
                public String processPropertyName(Class beanClass, String name) {
                    if (null != (entry.getValue().get(name))) {
                        return entry.getValue().get(name);
                    }
                    return name;
                }
            });
        }

        return config;
    }

    /**
     * Find all the entries in json array with specific key
     */
    private Collection<Map.Entry> findEntry(JSONArray array, String key) {
        List<Map.Entry> entries = new ArrayList<Map.Entry>();
        for (Object obj : array) {
            if (obj instanceof JSONObject) {
                JSONObject jObj = (JSONObject) obj;
                for (Object entryObj : jObj.entrySet()) {
                    Map.Entry entry = (Map.Entry) entryObj;
                    if (entry.getKey().equals(key)) {
                        entries.add(entry);
                    }
                    if (entry.getValue() instanceof JSONArray) {
                        entries.addAll(findEntry((JSONArray) entry.getValue(), key));
                    }
                }
            } else { //JSONArray
                JSONArray jArray = (JSONArray) obj;
                entries.addAll(findEntry(jArray, key));
            }
        }
        return entries;
    }

}
